今天大概會聊到的範圍
- SideEffect
 - DisposableEffect
 
今天要講的東西是 Side-Effect,很多場景下都有更好的 solution。以下多是為了舉例而寫出來的程式,請在實際使用前思考正確架構。
前情提要,上一篇有講到如果我們在判斷沒有 persmission 時,就主動去 launch 並且試圖取得 permission :
@Composable
fun CameraScreen() {
   
    // 取得 context
    val context = LocalContext.current
    
    // 檢查完 permission 後將答案存在 remember 中
    var hasPermission by remember {
        mutableStateOf(checkPermissionsGranted(context))
    }
    val permissionRequester = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
    ) { isGranted: Boolean ->
       
        hasPermission = isGranted
    }
    
    if (hasPermission) {
        CameraView()
    } else {
        permissionRequester.launch(Manifest.permission.CAMERA) // error
     }
}
程式會報錯,會出現 Launcher has not been initialized 的錯誤。
原因是這樣的,composable function 和一般 function 不同。他們的執行時間、執行次數都是由 compose-runtime 所決定。以這個例子為例,在  launcher ( permissionRequester )  還沒準備好的時候,CameraScreen 就會先被執行了好幾次,因此會出現錯誤。
在 Compose 中,每一個 composable function 都可以視為一個 "接收參數並產生畫面" 的 function。在這個定義下,所有與這個流程無關的資料改變、行為都屬於 "副作用" side-effect。
舉個例: ( anti-pattern 請勿學習 )
var count = 0
@Composable
fun SideEffectAntiPattern() {    
    count ++ 
}
因為我們無法控制 composable function 的執行次數、生命週期。當 composable function 內有異動到外部的資料、或是發出異步的 function (例如 network request ) 都有可能產生問題。
SideEffect 是一個可以讓我們用來明確定義 side effect  效果的 composable function。 他可以讓我們要執行的事情在 composition 完成後執行。
var count = 0
@Composable
fun SideEffectComposable() {    
    
    SideEffect {
        count ++ 
    }
}
SideEffect 還可以用來將 compose state 傳遞給無法使用 compose state 的物件:
@Composable
fun SideEffectComposable(window: Window) {
    
    var color by remember { mutableStateOf(Color.Blue) }
    
    SideEffect {
        window.statusBarColor = color.toArgb()
    
    }
}
有些時後,我們不僅是要在 composition 完成時觸發事件,還要再 composable 要被回收時做一些 clean up 的動作。這時我們可以使用和 SideEffect 很類似的 DisposableEffect
val lifecycleOwner = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycleOwner) {
    val lifecycle = lifecycleOwner
    val observer = LifecycleEventObserver { _, event ->
        when (event) {
            Lifecycle.Event.ON_PAUSE -> {
                // do things
            }
            Lifecycle.Event.ON_RESUME -> {
                // do things
            }
            Lifecycle.Event.ON_DESTROY -> {
                // do things
            }
        }
    }
    lifecycle.addObserver(observer)
    onDispose {
        lifecycle.removeObserver(observer)
    }
}
DisposableEffect 可以接收(也可以不提供)一個 key。當 composition 完成時,DisposableEffect 後面的 lambda 會執行。在 lambda 的最後,需要透過 onDispose function 產生一個 DisposableEffectResult。當 key 改變或是當 composable 被清除掉時,DisposableEffectResult  中的行為就會觸發。
透過 SideEffect 和 DisposableEffect 可以間接碰觸到 Composable 的生命週期,在正確的時機點執行。但這些可能都不是最好的做法(case by case),在 Compose 中,最好還是讓 composable function 維持只有處理畫面的部分,行為等可以往上提升到適當的階層,或是透過 view model 等角色拉到外部去觸發。
回到取得權限,因為我們需要在 composition 完成後執行 launcher.launch ,我們只需要簡單將 launch 行為透過 SideEffect 包裝起來就可以了。
    if (hasPermission) {
        CameraView()
    } else {
        SideEffect {
            permissionRequester.launch(Manifest.permission.CAMERA)
        }
     }
今天的文章最後感覺沒什麼收尾,那是因為我在研究 SideEffect 的過程,還有看到一系列的 state & effect API,不過感覺還沒辦法講得很明白,所以決定之後再分享。